Making TCPSocket.new "Happy"!
Making TCPSocket.new "Happy"!
I introduced Happy Eyeballs Version 2 (RFC8305) (hereafter HEv2) into the socket library's Socket.tcp and TCPSocket.new. This algorithm addresses the issue of delays that occur when DNS resolution for one address family takes too long, or when one of the candidate IP addresses is unavailable, preventing fallback to other options.
At RubyKaigi 2024, I presented how HEv2 was implemented in Socket.tcp (written in Ruby). And I initially planned to apply the same approach to TCPSocket.new (written in C). However, things did not go as expected, and the work began with reimplementing Socket.tcp itself.
This was a deeply rewarding project—so much so that it turned me, a Rubyist with an interest in networking, into a Ruby committer.
In this presentation, I will revisit HEv2, particularly focusing on TCPSocket.new, and share the journey of how it was implemented, merged, and eventually released as part of Ruby 3.4.
Socket.tcp と TCPSocket.new に Happy Eyeballs Version 2 (HEv2)を導入した
Happy Eyeballsとは
Happy Eyeballsでは、 通信開始当初からIPv6とIPv4の両方のプロトコルを用いて通信先と接続を行い、 先に接続に成功した方のプロトコルから得られた結果をユーザーへ出力します。
前回の発表
RubyKaigi 2024
HEv2をSocket.tcp(Rubyで記述)に実装した方法の発表
当初は同様のアプローチをTCPSocket.new(Cで記述)に適用する予定だった
→ 思うようにいかなかったため Socket.tcp の再実装から
TCPSocket.new に焦点を当てつつRuby 3.4にリリースされるまでの過程
Ruby3.4のリリースノート
Ruby 3.3 までは上記の2つのメソッドは名前解決と接続試行をシリアルに実行していました。Happy Eyeballs Version 2 のアルゴリズムでは以下のように実行します。
IPv6とIPv4の名前解決を同時実行する。
解決されたIPアドレスへの接続を、IPv6を優先して、250ミリ秒間隔で並行して試みる。
最初に成功した接続を返し、他の接続はキャンセルする。
このアルゴリズムによって、特定のプロトコルや IP アドレスが遅延した
標準で有効になる
yana-gi.icon従来がどうだったかイメージがついていない
プロと読み解くRuby 3.4 NEWS - STORES Product Blog
従来は、IPv6 や IPv4 に逐次で接続を試みていました。この方法だと、何らかの理由で IPv6 の通信に問題がある環境で先に IPv6 で接続をしようとすると、それがタイムアウトするまで IPv4 の接続試行に移らないので、とても時間がかかっていました。Ruby 3.4 は IPv6 と IPv4 に同時並行で接続するので、IPv6 がダメでもすぐに IPv4 が反応してくれて、タイムアウトを待つことなく接続確立するはずです。
まあ実際のところ、ユーザメリットよりは、社会が IPv4 から IPv6 に徐々に移行していくために必要な移行措置という印象が個人的には強いです。Happy Eyeballs は社会的責任。Ruby は社会的責任を果たしていてえらい。
yana-gi.icon Socketがまだ全然理解できていない
TokyoWomen.rb で聞いた講演
簡単なクライアントとサーバーのやりとりのプログラムで、やり取りの間に何が行われているのかのコードを追っている
Socket.tcpや TCPSocket.new も出てくる
ChatGTPに書いてもらった階層
あなたのコード
└─ Net::HTTP / OpenURI / Faraday などのライブラリ
└─ TCPSocket / UDPSocket
└─ Socket(Rubyのクラス)
└─ OSのソケットAPI(Cレベル)
ソケットというのは通信路の末端です。たとえば 1対1 の通信では、まず通信路の両端にひとつずつソケットをつくり、それらのソケットを接続することによって通信路が確立し、相互に通信できるようになります。この接続時に、一方のソケットにもう一方のソケットの場所を教えてやる必要がありますが、この場所を指定するものがソケットアドレスです。
ソケットアドレスはソケットの種類によって中身が異なります。たとえば TCP では IP アドレスとポート番号ですし、 Unix ドメインソケットではソケットファイルを指すパス名です。
ソケットアドレスを取り扱うための便利で高水準なクラスとして Addrinfo があります。
イメージしやすかった図
https://gyazo.com/661e87fd2b287a306f35b476f3e9bb32
https://gyazo.com/a5973103d70aaf5b836395fa14236679
Socket
Socket > BasicSocket
Socket.tcp
tcp(host, port) -> Socket
TCP/IP で host:port に接続するソケットオブジェクトを作成します。
TCPSocket
TCPSocket > IPSocket > BasicSocket
TCPSocket.new
open(host, service) -> TCPSocket
tcp(host, port) {|socket| ... } -> object
host で指定したホストの service で指定したポートと接続したソケットを返します
Socket.tcpとTCPSocket.newの違い
Socket.tcp
ブロックを渡せる、ブロック終了時にソケットをクローズする
自動的にソケットを作成し、ブロック終了時にソケットがクローズされる
code:ruby
require 'socket'
Socket.tcp('localhost', 2000) do |socket|
socket.puts "Hello!"
while line = socket.gets
puts line
end
end
TCPSocket.new
TCPSocketクラスのインスタンスを直接生成し、TCPクライアントソケットを作成する
ソケットの生成・接続・送受信を明示的に行う必要がある
code:ruby
require 'socket'
socket = TCPSocket.new('localhost', 2000)
socket.puts "Hello!"
while line = socket.gets
puts line
end
socket.close
yana-gi.iconこれも読みたい
ソケット通信の仕組みをスライド図解と Go 実装でまとめてみる